<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>webauthn test</title>
</head>
<body onload="init()">
<h1>webauthn test</h1>
<p>
This is a demo/test page for generating FIDO keys and signatures in SSH
formats. The page initially displays a form to generate a FIDO key and
convert it to a SSH public key.
</p>
<p>
Once a key has been generated, an additional form will be displayed to
allow signing of data using the just-generated key. The data may be signed
as either a raw SSH signature or wrapped in a sshsig message (the latter is
easier to test using command-line tools.
</p>
<p>
Lots of debugging is printed along the way.
</p>
<h2>Enroll</h2>
<span id="error" style="color: #800; font-weight: bold; font-size: 150%;"></span>
<form id="enrollform">
<table>
<tr>
<td><b>Username:</b></td>
<td><input id="username" type="text" size="20" name="user" value="test" /></td>
</tr>
<tr><td></td><td><input id="assertsubmit" type="submit" value="submit" /></td></tr>
</table>
</form>
<span id="enrollresult" style="visibility: hidden;">
<h2>clientData</h2>
<pre id="enrollresultjson" style="color: #008; font-family: monospace;"></pre>
<h2>attestationObject raw</h2>
<pre id="enrollresultraw" style="color: #008; font-family: monospace;"></pre>
<h2>attestationObject</h2>
<pre id="enrollresultattestobj" style="color: #008; font-family: monospace;"></pre>
<h2>key handle</h2>
<pre id="keyhandle" style="color: #008; font-family: monospace;"></pre>
<h2>authData raw</h2>
<pre id="enrollresultauthdataraw" style="color: #008; font-family: monospace;"></pre>
<h2>authData</h2>
<pre id="enrollresultauthdata" style="color: #008; font-family: monospace;"></pre>
<h2>SSH pubkey blob</h2>
<pre id="enrollresultpkblob" style="color: #008; font-family: monospace;"></pre>
<h2>SSH pubkey string</h2>
<pre id="enrollresultpk" style="color: #008; font-family: monospace;"></pre>
<h2>SSH private key string</h2>
<pre id="enrollresultprivkey" style="color: #008; font-family: monospace;"></pre>
</span>
<span id="assertsection" style="visibility: hidden;">
<h2>Assert</h2>
<form id="assertform">
<span id="asserterror" style="color: #800; font-weight: bold;"></span>
<table>
<tr>
<td><b>Data to sign:</b></td>
<td><input id="message" type="text" size="20" name="message" value="test" /></td>
</tr>
<tr>
<td><input id="message_sshsig" type="checkbox" checked /> use sshsig format</td>
</tr>
<tr>
<td><b>Signature namespace:</b></td>
<td><input id="message_namespace" type="text" size="20" name="namespace" value="test" /></td>
</tr>
<tr><td></td><td><input type="submit" value="submit" /></td></tr>
</table>
</form>
</span>
<span id="assertresult" style="visibility: hidden;">
<h2>clientData</h2>
<pre id="assertresultjson" style="color: #008; font-family: monospace;"></pre>
<h2>signature raw</h2>
<pre id="assertresultsigraw" style="color: #008; font-family: monospace;"></pre>
<h2>authenticatorData raw</h2>
<pre id="assertresultauthdataraw" style="color: #008; font-family: monospace;"></pre>
<h2>authenticatorData</h2>
<pre id="assertresultauthdata" style="color: #008; font-family: monospace;"></pre>
<h2>signature in SSH format</h2>
<pre id="assertresultsshsigraw" style="color: #008; font-family: monospace;"></pre>
<h2>signature in SSH format (base64 encoded)</h2>
<pre id="assertresultsshsigb64" style="color: #008; font-family: monospace;"></pre>
</span>
</body>
<script>
// ------------------------------------------------------------------
// a crappy CBOR decoder - 20200401 djm@openbsd.org

var CBORDecode = function(buffer) {
	this.buf = buffer
	this.v = new DataView(buffer)
	this.offset = 0
}

CBORDecode.prototype.empty = function() {
	return this.offset >= this.buf.byteLength
}

CBORDecode.prototype.getU8 = function() {
	let r = this.v.getUint8(this.offset)
	this.offset += 1
	return r
}

CBORDecode.prototype.getU16 = function() {
	let r = this.v.getUint16(this.offset)
	this.offset += 2
	return r
}

CBORDecode.prototype.getU32 = function() {
	let r = this.v.getUint32(this.offset)
	this.offset += 4
	return r
}

CBORDecode.prototype.getU64 = function() {
	let r = this.v.getUint64(this.offset)
	this.offset += 8
	return r
}

CBORDecode.prototype.getCBORTypeLen = function() {
	let tl, t, l
	tl = this.getU8()
	t = (tl & 0xe0) >> 5
	l = tl & 0x1f
	return [t, this.decodeInteger(l)]
}

CBORDecode.prototype.decodeInteger = function(len) {
	switch (len) {
	case 0x18: return this.getU8()
	case 0x19: return this.getU16()
	case 0x20: return this.getU32()
	case 0x21: return this.getU64()
	default:
		if (len <= 23) {
			return len
		}
		throw new Error("Unsupported int type 0x" + len.toString(16))
	}
}

CBORDecode.prototype.decodeNegint = function(len) {
	let r = -(this.decodeInteger(len) + 1)
	return r
}

CBORDecode.prototype.decodeByteString = function(len) {
	let r = this.buf.slice(this.offset, this.offset + len)
	this.offset += len
	return r
}

CBORDecode.prototype.decodeTextString = function(len) {
	let u8dec = new TextDecoder('utf-8')
	r = u8dec.decode(this.decodeByteString(len))
	return r
}

CBORDecode.prototype.decodeArray = function(len, level) {
	let r = []
	for (let i = 0; i < len; i++) {
		let v = this.decodeInternal(level)
		r.push(v)
		// console.log("decodeArray level " + level.toString() + " index " + i.toString() + " value " + JSON.stringify(v))
	}
	return r
}

CBORDecode.prototype.decodeMap = function(len, level) {
	let r = {}
	for (let i = 0; i < len; i++) {
		let k = this.decodeInternal(level)
		let v = this.decodeInternal(level)
		r[k] = v
		// console.log("decodeMap level " + level.toString() + " key " + k.toString() + " value " + JSON.stringify(v))
		// XXX check string keys, duplicates
	}
	return r
}

CBORDecode.prototype.decodePrimitive = function(t) {
	switch (t) {
	case 20: return false
	case 21: return true
	case 22: return null
	case 23: return undefined
	default:
		throw new Error("Unsupported primitive 0x" + t.toString(2))
	}
}

CBORDecode.prototype.decodeInternal = function(level) {
	if (level > 256) {
		throw new Error("CBOR nesting too deep")
	}
	let t, l, r
	[t, l] = this.getCBORTypeLen()
	// console.log("decode level " + level.toString() + " type " + t.toString() + " len " + l.toString())
	switch (t) {
		case 0:
			r = this.decodeInteger(l)
			break
		case 1:
			r = this.decodeNegint(l)
			break
		case 2:
			r = this.decodeByteString(l)
			break
		case 3:
			r = this.decodeTextString(l)
			break
		case 4:
			r = this.decodeArray(l, level + 1)
			break
		case 5:
			r = this.decodeMap(l, level + 1)
			break
		case 6:
			console.log("XXX ignored semantic tag " + this.decodeInteger(l).toString())
			break;
		case 7:
			r = this.decodePrimitive(l)
			break
		default:
			throw new Error("Unsupported type 0x" + t.toString(2) + " len " + l.toString())
	}
	// console.log("decode level " + level.toString() + " value " + JSON.stringify(r))
	return r
}

CBORDecode.prototype.decode = function() {
	return this.decodeInternal(0)
}

// ------------------------------------------------------------------
// a crappy SSH message packer - 20200401 djm@openbsd.org

var SSHMSG = function() {
	this.r = []
}

SSHMSG.prototype.length = function() {
	let len = 0
	for (buf of this.r) {
		len += buf.length
	}
	return len
}

SSHMSG.prototype.serialise = function() {
	let r = new ArrayBuffer(this.length())
	let v = new Uint8Array(r)
	let offset = 0
	for (buf of this.r) {
		v.set(buf, offset)
		offset += buf.length
	}
	if (offset != r.byteLength) {
		throw new Error("djm can't count")
	}
	return r
}

SSHMSG.prototype.serialiseBase64 = function(v) {
	let b = this.serialise()
	return btoa(String.fromCharCode(...new Uint8Array(b)));
}

SSHMSG.prototype.putU8 = function(v) {
	this.r.push(new Uint8Array([v]))
}

SSHMSG.prototype.putU32 = function(v) {
	this.r.push(new Uint8Array([
		(v >> 24) & 0xff,
		(v >> 16) & 0xff,
		(v >> 8) & 0xff,
		(v & 0xff)
	]))
}

SSHMSG.prototype.put = function(v) {
	this.r.push(new Uint8Array(v))
}

SSHMSG.prototype.putStringRaw = function(v) {
	let enc = new TextEncoder();
	let venc = enc.encode(v)
	this.put(venc)
}

SSHMSG.prototype.putString = function(v) {
	let enc = new TextEncoder();
	let venc = enc.encode(v)
	this.putU32(venc.length)
	this.put(venc)
}

SSHMSG.prototype.putSSHMSG = function(v) {
	let msg = v.serialise()
	this.putU32(msg.byteLength)
	this.put(msg)
}

SSHMSG.prototype.putBytes = function(v) {
	this.putU32(v.byteLength)
	this.put(v)
}

SSHMSG.prototype.putECPoint = function(x, y) {
	let x8 = new Uint8Array(x)
	let y8 = new Uint8Array(y)
	this.putU32(1 + x8.length + y8.length)
	this.putU8(0x04) // Uncompressed point format.
	this.put(x8)
	this.put(y8)
}

// ------------------------------------------------------------------
// webauthn to SSH glue - djm@openbsd.org 20200408

function error(msg, ...args) {
	document.getElementById("error").innerText = msg
	console.log(msg)
	for (const arg of args) {
		console.dir(arg)
	}
}
function hexdump(buf) {
	const hex = Array.from(new Uint8Array(buf)).map(
		b => b.toString(16).padStart(2, "0"))
	const fmt = new Array()
	for (let i = 0; i < hex.length; i++) {
		if ((i % 16) == 0) {
			// Prepend length every 16 bytes.
			fmt.push(i.toString(16).padStart(4, "0"))
			fmt.push("  ")
		}
		fmt.push(hex[i])
		fmt.push(" ")
		if ((i % 16) == 15) {
			fmt.push("\n")
		}
	}
	return fmt.join("")
}
function enrollform_submit(event) {
	event.preventDefault();
	console.log("submitted")
	username = event.target.elements.username.value
	if (username === "") {
		error("no username specified")
		return false
	}
	enrollStart(username)
}
function enrollStart(username) {
	let challenge = new Uint8Array(32)
	window.crypto.getRandomValues(challenge)
	let userid = new Uint8Array(8)
	window.crypto.getRandomValues(userid)

	console.log("challenge:" + btoa(challenge))
	console.log("userid:" + btoa(userid))

	let pkopts = {
		challenge: challenge,
		rp: {
			name: window.location.host,
			id: window.location.host,
		},
		user: {
			id: userid,
			name: username,
			displayName: username,
		},
		authenticatorSelection: {
			authenticatorAttachment: "cross-platform",
			userVerification: "discouraged",
		},
		pubKeyCredParams: [{alg: -7, type: "public-key"}], // ES256
		timeout: 30 * 1000,
	};
	console.dir(pkopts)
	window.enrollOpts = pkopts
	let credpromise = navigator.credentials.create({ publicKey: pkopts });
	credpromise.then(enrollSuccess, enrollFailure)
}
function enrollFailure(result) {
	error("Enroll failed", result)
}
function enrollSuccess(result) {
	console.log("Enroll succeeded")
	console.dir(result)
	window.enrollResult = result
	document.getElementById("enrollresult").style.visibility = "visible"

	// Show the clientData
	let u8dec = new TextDecoder('utf-8')
	clientData = u8dec.decode(result.response.clientDataJSON)
	document.getElementById("enrollresultjson").innerText = clientData

	// Show the raw key handle.
	document.getElementById("keyhandle").innerText = hexdump(result.rawId)

	// Decode and show the attestationObject
	document.getElementById("enrollresultraw").innerText = hexdump(result.response.attestationObject)
	let aod = new CBORDecode(result.response.attestationObject)
	let attestationObject = aod.decode()
	console.log("attestationObject")
	console.dir(attestationObject)
	document.getElementById("enrollresultattestobj").innerText = JSON.stringify(attestationObject)

	// Decode and show the authData
	document.getElementById("enrollresultauthdataraw").innerText = hexdump(attestationObject.authData)
	let authData = decodeAuthenticatorData(attestationObject.authData, true)
	console.log("authData")
	console.dir(authData)
	window.enrollAuthData = authData
	document.getElementById("enrollresultauthdata").innerText = JSON.stringify(authData)

	// Reformat the pubkey as a SSH key for easy verification
	window.rawKey = reformatPubkey(authData.attestedCredentialData.credentialPublicKey, window.enrollOpts.rp.id)
	console.log("SSH pubkey blob")
	console.dir(window.rawKey)
	document.getElementById("enrollresultpkblob").innerText = hexdump(window.rawKey)
	let pk64 = btoa(String.fromCharCode(...new Uint8Array(window.rawKey)));
	let pk = "sk-ecdsa-sha2-nistp256@openssh.com " + pk64
	document.getElementById("enrollresultpk").innerText = pk

	// Format a private key too.
	flags = 0x01 // SSH_SK_USER_PRESENCE_REQD
	window.rawPrivkey = reformatPrivkey(authData.attestedCredentialData.credentialPublicKey, window.enrollOpts.rp.id, result.rawId, flags)
	let privkeyFileBlob = privkeyFile(window.rawKey, window.rawPrivkey, window.enrollOpts.user.name, window.enrollOpts.rp.id)
	let privk64 = btoa(String.fromCharCode(...new Uint8Array(privkeyFileBlob)));
	let privkey = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + wrapString(privk64, 70) + "-----END OPENSSH PRIVATE KEY-----\n"
	document.getElementById("enrollresultprivkey").innerText = privkey

	// Success: show the assertion form.
	document.getElementById("assertsection").style.visibility = "visible"
}

function decodeAuthenticatorData(authData, expectCred) {
	let r = new Object()
	let v = new DataView(authData)

	r.rpIdHash = authData.slice(0, 32)
	r.flags = v.getUint8(32)
	r.signCount = v.getUint32(33)

	// Decode attestedCredentialData if present.
	let offset = 37
	let acd = new Object()
	if (expectCred) {
		acd.aaguid = authData.slice(offset, offset+16)
		offset += 16
		let credentialIdLength = v.getUint16(offset)     
		offset += 2
		acd.credentialIdLength = credentialIdLength
		acd.credentialId = authData.slice(offset, offset+credentialIdLength)
		offset += credentialIdLength
		r.attestedCredentialData = acd
	}
	console.log("XXXXX " + offset.toString())
	let pubkeyrest = authData.slice(offset, authData.byteLength)
	let pkdecode = new CBORDecode(pubkeyrest)
	if (expectCred) {
		// XXX unsafe: doesn't mandate COSE canonical format.
		acd.credentialPublicKey = pkdecode.decode()
	}
	if (!pkdecode.empty()) {
		// Decode extensions if present.
		r.extensions = pkdecode.decode()
	}
	return r
}

function wrapString(s, l) {
	ret = ""
	for (i = 0; i < s.length; i += l) {
		ret += s.slice(i, i + l) + "\n"
	}
	return ret
}

function checkPubkey(pk) {
	// pk is in COSE format. We only care about a tiny subset.
	if (pk[1] != 2) {
		console.dir(pk)
		throw new Error("pubkey is not EC")
	}
	if (pk[-1] != 1) {
		throw new Error("pubkey is not in P256")
	}
	if (pk[3] != -7) {
		throw new Error("pubkey is not ES256")
	}
	if (pk[-2].byteLength != 32 || pk[-3].byteLength != 32) {
		throw new Error("pubkey EC coords have bad length")
	}
}

function reformatPubkey(pk, rpid) {
	checkPubkey(pk)
	let msg = new SSHMSG()
	msg.putString("sk-ecdsa-sha2-nistp256@openssh.com")	// Key type
	msg.putString("nistp256")				// Key curve
	msg.putECPoint(pk[-2], pk[-3])				// EC key
	msg.putString(rpid)					// RP ID
	return msg.serialise()
}

function reformatPrivkey(pk, rpid, kh, flags) {
	checkPubkey(pk)
	let msg = new SSHMSG()
	msg.putString("sk-ecdsa-sha2-nistp256@openssh.com")	// Key type
	msg.putString("nistp256")				// Key curve
	msg.putECPoint(pk[-2], pk[-3])				// EC key
	msg.putString(rpid)					// RP ID
	msg.putU8(flags)					// flags
	msg.putBytes(kh)					// handle
	msg.putString("")					// reserved
	return msg.serialise()
}

function privkeyFile(pub, priv, user, rp) {
	let innerMsg = new SSHMSG()
	innerMsg.putU32(0xdeadbeef)				// check byte
	innerMsg.putU32(0xdeadbeef)				// check byte
	innerMsg.put(priv)					// privkey
	innerMsg.putString("webauthn.html " + user + "@" + rp)	// comment
	// Pad to cipher blocksize (8).
	p = 1
	while (innerMsg.length() % 8 != 0) {
		innerMsg.putU8(p++)
	}
	let msg = new SSHMSG()
	msg.putStringRaw("openssh-key-v1")			// Magic
	msg.putU8(0)						// \0 terminate
	msg.putString("none")					// cipher
	msg.putString("none")					// KDF
	msg.putString("")					// KDF options
	msg.putU32(1)						// nkeys
	msg.putBytes(pub)					// pubkey
	msg.putSSHMSG(innerMsg)					// inner
	//msg.put(innerMsg.serialise())				// inner
	return msg.serialise()
}

async function assertform_submit(event) {
	event.preventDefault();
	console.log("submitted")
	message = event.target.elements.message.value
	if (message === "") {
		error("no message specified")
		return false
	}
	let enc = new TextEncoder()
	let encmsg = enc.encode(message)
	window.assertSignRaw = !event.target.elements.message_sshsig.checked
	console.log("using sshsig ", !window.assertSignRaw)
	if (window.assertSignRaw) {
		assertStart(encmsg)
		return
	}
	// Format a sshsig-style message.
	window.sigHashAlg = "sha512"
	let msghash = await crypto.subtle.digest("SHA-512", encmsg);
	console.log("raw message hash")
	console.dir(msghash)
	window.sigNamespace = event.target.elements.message_namespace.value
	let sigbuf = new SSHMSG()
	sigbuf.put(enc.encode("SSHSIG"))
	sigbuf.putString(window.sigNamespace)
	sigbuf.putU32(0) // Reserved string
	sigbuf.putString(window.sigHashAlg)
	sigbuf.putBytes(msghash)
	let msg = sigbuf.serialise()
	console.log("sigbuf")
	console.dir(msg)
	assertStart(msg)
}

function assertStart(message) {
	let assertReqOpts = {
		challenge: message,
		rpId: window.location.host,
		allowCredentials: [{
			type: 'public-key',
			id: window.enrollResult.rawId,
		}],
		userVerification: "discouraged",
		timeout: (30 * 1000),
	}
	console.log("assertReqOpts")
	console.dir(assertReqOpts)
	window.assertReqOpts = assertReqOpts
	let assertpromise = navigator.credentials.get({
		publicKey: assertReqOpts
	});
	assertpromise.then(assertSuccess, assertFailure)
}
function assertFailure(result) {
	error("Assertion failed", result)
}
function linewrap(s) {
	const linelen = 70
	let ret = ""
	for (let i = 0; i < s.length; i += linelen) {
		end = i + linelen
		if (end > s.length) {
			end = s.length
		}
		if (i > 0) {
			ret += "\n"
		}
		ret += s.slice(i, end)
	}
	return ret + "\n"
}
function assertSuccess(result) {
	console.log("Assertion succeeded")
	console.dir(result)
	window.assertResult = result
	document.getElementById("assertresult").style.visibility = "visible"

	// show the clientData.
	let u8dec = new TextDecoder('utf-8')
	clientData = u8dec.decode(result.response.clientDataJSON)
	document.getElementById("assertresultjson").innerText = clientData

	// show the signature.
	document.getElementById("assertresultsigraw").innerText = hexdump(result.response.signature)
	
	// decode and show the authData.
	document.getElementById("assertresultauthdataraw").innerText = hexdump(result.response.authenticatorData)
	authData = decodeAuthenticatorData(result.response.authenticatorData, false)
	document.getElementById("assertresultauthdata").innerText = JSON.stringify(authData)

	// Parse and reformat the signature to an SSH style signature.
	let sshsig = reformatSignature(result.response.signature, clientData, authData)
	document.getElementById("assertresultsshsigraw").innerText = hexdump(sshsig)
	let sig64 = btoa(String.fromCharCode(...new Uint8Array(sshsig)));
	if (window.assertSignRaw) {
		document.getElementById("assertresultsshsigb64").innerText = sig64
	} else {
		document.getElementById("assertresultsshsigb64").innerText =
		    "-----BEGIN SSH SIGNATURE-----\n" + linewrap(sig64) +
		    "-----END SSH SIGNATURE-----\n";
	}
}

function reformatSignature(sig, clientData, authData) {
	if (sig.byteLength < 2) {
		throw new Error("signature is too short")
	}
	let offset = 0
	let v = new DataView(sig)
	// Expect an ASN.1 SEQUENCE that exactly spans the signature.
	if (v.getUint8(offset) != 0x30) {
		throw new Error("signature not an ASN.1 sequence")
	}
	offset++
	let seqlen = v.getUint8(offset)
	offset++
	if ((seqlen & 0x80) != 0 || seqlen != sig.byteLength - offset) {
		throw new Error("signature has unexpected length " + seqlen.toString() + " vs expected " + (sig.byteLength - offset).toString())
	}

	// Parse 'r' INTEGER value.
	if (v.getUint8(offset) != 0x02) {
		throw new Error("signature r not an ASN.1 integer")
	}
	offset++
	let rlen = v.getUint8(offset)
	offset++
	if ((rlen & 0x80) != 0 || rlen > sig.byteLength - offset) {
		throw new Error("signature r has unexpected length " + rlen.toString() + " vs buffer " + (sig.byteLength - offset).toString())
	}
	let r = sig.slice(offset, offset + rlen)
	offset += rlen
	console.log("sig_r")
	console.dir(r)

	// Parse 's' INTEGER value.
	if (v.getUint8(offset) != 0x02) {
		throw new Error("signature r not an ASN.1 integer")
	}
	offset++
	let slen = v.getUint8(offset)
	offset++
	if ((slen & 0x80) != 0 || slen > sig.byteLength - offset) {
		throw new Error("signature s has unexpected length " + slen.toString() + " vs buffer " + (sig.byteLength - offset).toString())
	}
	let s = sig.slice(offset, offset + slen)
	console.log("sig_s")
	console.dir(s)
	offset += slen

	if (offset != sig.byteLength) {
		throw new Error("unexpected final offset during signature parsing " + offset.toString() + " expected " + sig.byteLength.toString())
	}
	
	// Reformat as an SSH signature.
	let clientDataParsed = JSON.parse(clientData)
	let innersig = new SSHMSG()
	innersig.putBytes(r)
	innersig.putBytes(s)

	let rawsshsig = new SSHMSG()
	rawsshsig.putString("webauthn-sk-ecdsa-sha2-nistp256@openssh.com")
	rawsshsig.putSSHMSG(innersig)
	rawsshsig.putU8(authData.flags)
	rawsshsig.putU32(authData.signCount)
	rawsshsig.putString(clientDataParsed.origin)
	rawsshsig.putString(clientData)
	if (authData.extensions == undefined) {
		rawsshsig.putU32(0)
	} else {
		rawsshsig.putBytes(authData.extensions)
	}

	if (window.assertSignRaw) {
		return rawsshsig.serialise()
	}
	// Format as SSHSIG.
	let enc = new TextEncoder()
	let sshsig = new SSHMSG()
	sshsig.put(enc.encode("SSHSIG"))
	sshsig.putU32(0x01) // Signature version.
	sshsig.putBytes(window.rawKey)
	sshsig.putString(window.sigNamespace)
	sshsig.putU32(0) // Reserved string
	sshsig.putString(window.sigHashAlg)
	sshsig.putBytes(rawsshsig.serialise())
	return sshsig.serialise()
}

function toggleNamespaceVisibility() {
	const assertsigtype = document.getElementById('message_sshsig');
	const assertsignamespace = document.getElementById('message_namespace');
	assertsignamespace.disabled = !assertsigtype.checked;
}

function init() {
	if (document.location.protocol != "https:") {
		error("This page must be loaded via https")
		const assertsubmit = document.getElementById('assertsubmit')
		assertsubmit.disabled = true
	}
	const enrollform = document.getElementById('enrollform');
	enrollform.addEventListener('submit', enrollform_submit);
	const assertform = document.getElementById('assertform');
	assertform.addEventListener('submit', assertform_submit);
	const assertsigtype = document.getElementById('message_sshsig');
	assertsigtype.onclick = toggleNamespaceVisibility;
}
</script>

</html>
